package org.jmap.geom;

import org.jmap.core.Extent;
import org.jmap.core.MapPoint;
import org.jmap.core.Util;
import org.jmap.geom.Point;

import org.osgeo.proj4j.CoordinateReferenceSystem;

public abstract class AbstractGeometry implements Geometry {

	private Geometry parent;
	private Extent extent;
	private String id;
	private String SRID;
	
	/**
	 * abstract base class for all <code>Geometry</code> types, this class implements
	 * common functionality of the <code>Geometry</code> interface
	 * 
	 * <p>each geometry object maintains a String unique identifier that can be used to
	 * identify it</code>
	 */
	public AbstractGeometry() {
		//generate a unique id
		//TODO other method?
		this.id = Util.generateUniqueId(this.getClass().getName() + "_");
	}
	
	public String getId() {
		return this.id;
	}
	
	public String getSRID() {
		return this.SRID;
	}
	
	public void setSRID(String id) {
		this.SRID = id;
	}
	
	@Override
	public Geometry getParent() {
		return this.parent;
	}

	@Override
	public Extent getExtent() {
		if (this.extent == null){
			this.computeExtent();
		}
		return this.extent;
	}

	@Override
	public void clearExtent() {
		this.extent = null;
		if (this.parent != null) {
			this.parent.clearExtent();
		}
	}

	protected void setExtent(Extent extent) {
		this.extent = extent.clone();
	}

	@Override
	public void extendExtent(Extent newExtent) {
		Extent extent = this.extent;
		if (extent == null) {
			setExtent(newExtent);
		} else {
			this.extent.extend(newExtent);
		}
	}
	
	public void dispose() {
		this.id = null;
		this.extent = null;
	}
	
	/**
	 * tests if a <code>MapPoint</code> is within the <code>Extent</code> 
	 * of this geometry given tolerance provided in lon and lat
	 * 
	 * @param point
	 * @param toleranceLon
	 * @param toleranceLat
	 * @return
	 */
	public boolean atPoint(MapPoint point, double toleranceLon, double toleranceLat) {
		boolean at = false;
		Extent extent = this.getExtent();
		if (extent != null && point != null) {
			double dX = toleranceLon;
			double dY = toleranceLat;
			
			Extent tolerantExtent = new Extent(this.extent.left - dX, this.extent.bottom - dY, 
												this.extent.left + dX, this.extent.top + dY);
			
			at = tolerantExtent.contains(point, true);
		}
		
		return at;
	}
	
	
	public void setParent(Geometry parent) {
		this.parent = parent;
	}

	public abstract Geometry transform(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem destinationCRS);

	@Override
	public abstract void computeExtent();

	public abstract double distanceTo(Geometry geometry, boolean edge);

	/**
	 * gets the center <code>Point</code> geometry
	 * 
	 * @return the center point
	 */
	public abstract Point getCentroid();
	
	public abstract boolean intersects(Geometry geometry, boolean edge);

	//public abstract Point[] getVertices(boolean endPoints);
	
	public abstract double getLength();
	
	public abstract double getArea();
	
	/**
	 * Method: OpenLayers.Geometry.segmentsIntersect
	 * Determine whether two line segments intersect.  Optionally calculates
	 *     and returns the intersection point.  This function is optimized for
	 *     cases where seg1.x2 >= seg2.x1 || seg2.x2 >= seg1.x1.  In those
	 *     obvious cases where there is no intersection, the function should
	 *     not be called.
	 *
	 * Parameters:
	 * seg1 - {Object} Object representing a segment with properties x1, y1, x2,
	 *     and y2.  The start point is represented by x1 and y1.  The end point
	 *     is represented by x2 and y2.  Start and end are ordered so that x1 < x2.
	 * seg2 - {Object} Object representing a segment with properties x1, y1, x2,
	 *     and y2.  The start point is represented by x1 and y1.  The end point
	 *     is represented by x2 and y2.  Start and end are ordered so that x1 < x2.
	 * options - {Object} Optional properties for calculating the intersection.
	 *
	 * Valid options:
	 * point - {Boolean} Return the intersection point.  If false, the actual
	 *     intersection point will not be calculated.  If true and the segments
	 *     intersect, the intersection point will be returned.  If true and
	 *     the segments do not intersect, false will be returned.  If true and
	 *     the segments are coincident, true will be returned.
	 * tolerance - {Number} If a non-null value is provided, if the segments are
	 *     within the tolerance distance, this will be considered an intersection.
	 *     In addition, if the point option is true and the calculated intersection
	 *     is within the tolerance distance of an end point, the endpoint will be
	 *     returned instead of the calculated intersection.  Further, if the
	 *     intersection is within the tolerance of endpoints on both segments, or
	 *     if two segment endpoints are within the tolerance distance of eachother
	 *     (but no intersection is otherwise calculated), an endpoint on the
	 *     first segment provided will be returned.
	 *
	 * @Return Boolean Point The two segments intersect.
	 *     If the point argument is true, the return will be the intersection
	 *     point or false if none exists.  If point is true and the segments
	 *     are coincident, return will be true (and the instersection is equal
	 *     to the shorter segment).
	 */
	public static boolean segmentsIntersect(Segment seg1, Segment seg2, SegmentIntersectOptions options) {
	    Point point = options.getPoint();
	    Point intersectionPoint = null;
	    double tolerance = options.tolerance;
	    boolean intersection = false;
	    double x11_21 = seg1.x1 - seg2.x1;
	    double y11_21 = seg1.y1 - seg2.y1;
	    double x12_11 = seg1.x2 - seg1.x1;
	    double y12_11 = seg1.y2 - seg1.y1;
	    double y22_21 = seg2.y2 - seg2.y1;
	    double x22_21 = seg2.x2 - seg2.x1;
	    double d = (y22_21 * x12_11) - (x22_21 * y12_11);
	    double n1 = (x22_21 * y11_21) - (y22_21 * x11_21);
	    double n2 = (x12_11 * y11_21) - (y12_11 * x11_21);
	    
	    if(d == 0) {
	        // parallel
	        if(n1 == 0 && n2 == 0) {
	            // coincident
	            intersection = true;
	        }
	    } else {
	        double along1 = n1 / d;
	        double along2 = n2 / d;
	        if(along1 >= 0 && along1 <= 1 && along2 >=0 && along2 <= 1) {
	            // intersect
	            if(point == null) {
	                intersection = true;
	            } else {
	                // calculate the intersection point
	                double x = seg1.x1 + (along1 * x12_11);
	                double y = seg1.y1 + (along1 * y12_11);
	                intersectionPoint = new Point(x, y);
	                intersection = true;
	            }
	        }
	    }
	    
	    if(tolerance == -1) {
	        double dist;
	        if(intersection) {
	            if(point != null) {
	                Segment[] segs = new Segment[] {seg1, seg2};
	                Segment seg;
	                double x, y;
	                // check segment endpoints for proximity to intersection
	                // set intersection to first endpoint within the tolerance
	                outer: for(int i=0; i<2; ++i) {
	                    seg = segs[i];
	                    for(int j=1; j<3; ++j) {
	                    	if (j == 1) {
	                    		x = seg.x1;
	                    	} else {
	                    		x = seg.x2;
	                    	}
	                    	if (j == 1) {
	                    		y  = seg.y1;
 	                    	} else {
 	                    		y = seg.y2;
 	                    	}
	                        dist = Math.sqrt(
	                            Math.pow(x - intersectionPoint.x, 2) +
	                            Math.pow(y - intersectionPoint.y, 2)
	                        );
	                        if(dist < tolerance) {
	                            intersectionPoint.x = x;
	                            intersectionPoint.y = y;
	                            break outer;
	                        }
	                    }
	                }
	                
	            }
	        } else {
	            // no calculated intersection, but segments could be within
	            // the tolerance of one another
	            Segment[] segs = new Segment[] {seg1, seg2};
	            
	            Segment source, target; 
	            double x, y, result;
	            Point p;
	            // check segment endpoints for proximity to intersection
	            // set intersection to first endpoint within the tolerance
	            outer: for(int i=0; i<2; ++i) {
	                source = segs[i];
	                target = segs[(i+1)%2];
	                for(int j=1; j<3; ++j) {
	                	if (j == 1) {
	                		x = source.x1;
	                		y = source.y1;
	                	} else {
	                		x = source.x2;
	                		y = source.y2;
	                	}
	                	p = new Point(x, y);
	                    
	                	
	                    result = distanceToSegment(p, target);
	                    if(result < tolerance) {
	                        if (point != null) {
	                            intersectionPoint = new Point(p.x, p.y);
	                        } else {
	                            intersection = true;
	                        }
	                        break outer;
	                    }
	                }
	            }
	        }
	    }
	    
	    return intersection;
	}
	
	public static double distanceToSegment(Point point, Segment segment) {
	    double x0 = point.x;
	    double y0 = point.y;
	    double x1 = segment.x1;
	    double y1 = segment.y1;
	    double x2 = segment.x2;
	    double y2 = segment.y2;
	    double dx = x2 - x1;
	    double dy = y2 - y1;
	    double along = ((dx * (x0 - x1)) + (dy * (y0 - y1))) /
	                (Math.pow(dx, 2) + Math.pow(dy, 2));
	    double x, y;
	    if(along <= 0.0) {
	        x = x1;
	        y = y1;
	    } else if(along >= 1.0) {
	        x = x2;
	        y = y2;
	    } else {
	        x = x1 + along * dx;
	        y = y1 + along * dy;
	    }
	    return Math.sqrt(Math.pow(x - x0, 2) + Math.pow(y - y0, 2));
	}
	
	
}

class SegmentIntersectOptions {
	Point point;
	double tolerance = -1d;
	
	public SegmentIntersectOptions(Point point, double tolerance) {
		this.point = point;
		this.tolerance = tolerance;
	}
	
	public Point getPoint() {
		return this.point;		
	}
	
	public double tolerance() {
		return this.tolerance;
	}
}
